package net.gazhi.delonix.rbac.service;

import java.sql.Timestamp;
import java.util.LinkedHashSet;
import java.util.UUID;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;

import net.gazhi.delonix.core.jpa.AbstractEntityService;
import net.gazhi.delonix.core.thread.ThreadContext;
import net.gazhi.delonix.rbac.entity.LoginUser;
import net.gazhi.delonix.rbac.entity.MenuItem;
import net.gazhi.delonix.rbac.entity.Ticket;
import net.gazhi.delonix.rbac.entity.User;
import net.gazhi.delonix.rbac.form.LoginForm;

@Service
public class TicketService extends AbstractEntityService<Ticket> {

	private static Log logger = LogFactory.getLog(TicketService.class);

	@Autowired
	private UserService userService;
	@Autowired
	private ActionService actionService;
	@Autowired
	private MenuItemService menuItemService;

	/**
	 * ticketNo 参数名
	 */
	public static final String PARAM_TICKET_NO = "_ticket_";

	/**
	 * 生成随机的唯一 ticket 串
	 * 
	 * @return
	 */
	public static String generateTokenString() {
		String s = UUID.randomUUID().toString().toUpperCase();
		return s.substring(0, 8) + s.substring(9, 13) + s.substring(14, 18) + s.substring(19, 23) + s.substring(24);
	}

	/**
	 * 依次尝试从请求 Parameter、Cookie、session 提取ticket
	 * 
	 * @param request
	 * @return
	 */
	public static final String pickUpTicketNo(HttpServletRequest request) {
		String ticketNo = request.getParameter(PARAM_TICKET_NO);
		if (ticketNo != null) {
			return ticketNo;
		}
		Cookie[] cookies = request.getCookies();
		if (cookies != null) {
			for (Cookie cookie : cookies) {
				if (cookie.getName().equals(PARAM_TICKET_NO)) {
					return cookie.getValue();
				}
			}
		}
		return (String) request.getSession().getAttribute(PARAM_TICKET_NO);
	}

	/**
	 * 根据 ticket 字符串查找 ticket 对象
	 * 
	 * @param ticketStr
	 * @return
	 */
	public Ticket findTicket(String ticketNo) {
		return (Ticket) this.createCriteria().add(Restrictions.eq("ticketNo", ticketNo)).setMaxResults(1).uniqueResult();
	}

	/**
	 * 
	 * 生成新的 access ticket<br>
	 * 流程：
	 * <ol>
	 * <li>根据用户名取得用户，如果不存在或者已删除，提示用户名或密码错误</li>
	 * <li><del>如果连续错误次数达到20以上，提示联系管理员重置密码</del></li>
	 * <li>如果连续错误次数是5的倍数，并且距离上次登录时间在60分钟内，并提示60分钟后再登录</li>
	 * <li>检查密码是否正确，如果错误，更新累计错误次数和最后登录时间，提示用户名或密码错误</li>
	 * <li>如果密码正确，检查用户是否被禁用，如果被禁用，提示联系管理员重置密码</li>
	 * <li>如果密码正确，并且没被禁用，则验证通过，累计错误数清0，更新最后登录时间，进入 access ticket 生成流程</li>
	 * </ol>
	 * 
	 * access ticket 生成过程
	 * <ol>
	 * <li>根据用户ID查获找最近的一个ticket</li>
	 * <li>如果不存在，进入下一步，如果存在，并且没有超期和注销，将 ticket 标记为注销</li>
	 * <li>生成新的ticket插入数据库并返回</li>
	 * </ol>
	 * 
	 * @param form
	 * @param errors
	 * @return
	 */
	@Transactional
	public Ticket generateTicket(LoginForm form, Errors errors) {
		if (errors.hasErrors()) {
			return null;
		}
		Timestamp now = new Timestamp(System.currentTimeMillis());
		User user = this.userService.findUser(form.getLoginName());
		// 如果不存在或者已删除，提示用户名或密码错误
		if (user == null || user.isDisabled()) {
			errors.reject("loginForm.invalidLoginNameOrPassword", "账号密码错误或者被禁用！");
			return null;
		}

		// 续错误次数是5的倍数，并且距离上次登录时间在60分钟内
		if (user.getInvalidLoginCount() > 0 && user.getInvalidLoginCount() % 5 == 0 && user.getLastLoginTime() != null && System.currentTimeMillis() - user.getLastLoginTime().getTime() < 600000) {
			errors.reject("loginForm.invalidLoginNameOrPassword", "账号密码错误或者被禁用！");
			return null;
		}
		// 密码错误，更新累计错误次数和最后登录时间
		String encryptedPasswd = this.userService.encryptPostedPasswd(form.getLoginPasswd());
		if (!user.getLoginPasswd().equals(encryptedPasswd)) {
			user.setInvalidLoginCount(user.getInvalidLoginCount() + 1);
			user.setLastLoginTime(now);
			dao.update(user);
			errors.reject("loginForm.invalidLoginNameOrPassword", "账号密码错误或者被禁用！");
			return null;
		}
		// 密码正确，但被禁用
		if (user.isDisabled()) {
			errors.reject("loginForm.invalidLoginNameOrPassword", "账号密码错误或者被禁用！");
			return null;
		}
		// 验证通过，错误数清零
		user.setInvalidLoginCount(0);
		user.setLastLoginTime(now);
		dao.update(user);

		String ticketNo = generateTokenString();
		long clientIp = ThreadContext.getClientIp();
		Ticket ticket = new Ticket(ticketNo, user.getId(), user.getLoginName(), clientIp, 7200);
		dao.save(ticket);

		dao.flushSession();
		return ticket;
	}

	/**
	 * 根据 ticket 创建 LoginUser
	 * 
	 * @param ticketStr
	 * @return 验证成功，返回对应的 loginUser；验证失败返回 null
	 */
	public LoginUser checkAndCreateLoginUser(HttpServletRequest request) {
		String ticketNo = pickUpTicketNo(request);
		if (!StringUtils.hasText(ticketNo)) {
			return null;
		}
		Ticket ticket = this.findTicket(ticketNo);
		if (ticket == null) {
			return null;
		}
		if (ticket.isLogouted() || ticket.getExpiryTime().before(new Timestamp(System.currentTimeMillis()))) {
			logger.warn("验证 ticket 失败：ticket_no=" + ticketNo + " 已失效！");
			return null;
		}
		User user = this.userService.get(ticket.getUserId());
		if (user == null || user.isDisabled()) {
			logger.warn("验证 ticket 失败：ticket_no=" + ticketNo + " 账户已停用！");
			return null;
		}

		LinkedHashSet<MenuItem> rootMemuItems = this.menuItemService.findRootMenuItems(user.getId());
		LoginUser loginUser = new LoginUser(ticketNo, user.getId(), user.getLoginName(), user.getName(), rootMemuItems);
		for (String path : this.actionService.findGrantPaths(loginUser)) {
			loginUser.getGrantedPaths().put(path, true);
		}
		// TODO: 还要预处理领域权限列表
		return loginUser;
	}

	/**
	 * 退出登录
	 * 
	 * @param loginUser
	 */
	@Transactional
	public void logout(LoginUser loginUser) {
		if (loginUser == null) {
			return;
		}
		Timestamp now = new Timestamp(System.currentTimeMillis());
		Ticket ticket = this.findTicket(loginUser.getTicketNo());
		if (ticket == null || ticket.isLogouted() || ticket.getExpiryTime().before(now)) {
			return;
		}
		ticket.setLogouted(true);
		ticket.setLogoutTime(now);
		dao.update(ticket);
	}

}
